Skip to content

fix(browser): ignore channel events in a tester opened as a top-level window (fix #9379)#9381

Open
simshanith wants to merge 2 commits into
vitest-dev:mainfrom
simshanith:simshanith/response-event-loop-guard
Open

fix(browser): ignore channel events in a tester opened as a top-level window (fix #9379)#9381
simshanith wants to merge 2 commits into
vitest-dev:mainfrom
simshanith:simshanith/response-event-loop-guard

Conversation

@simshanith

@simshanith simshanith commented Jan 3, 2026

Copy link
Copy Markdown

Description

Fixes #9379 — the recursive Unknown event: response:response:...:execute/cleanup storm in browser mode.

Root cause (traced)

My earlier explanation (BroadcastChannel echoing to the sender) was wrong, as pointed out in review — thanks. The actual mechanism:

  1. The orchestrator creates tester iframes with src = "/?sessionId=<id>&iframeId=<id>" (createTestIframe, packages/browser/src/client/orchestrator.ts).
  2. The dev server serves a fully functional tester page to any window that requests / with a live sessionId (packages/browser/src/node/serverTester.ts).
  3. An anchor's href="" resolves to the current URL including the query string. So a test that clicks <a href="" target="_blank"> — or ctrl/meta-clicks a relative link, which routers deliberately don't preventDefault() — opens a popup at the tester URL. The popup boots a second, top-level tester with the same sessionId and the same iframeId, joining the same BroadcastChannel.

From then on the two same-iframeId testers ping-pong:

  • The popup posts ready. The real tester receives it (the iframeId check passes — it's its own id), posts ack:ready, falls into the default: case (Unknown event: ready), and the finally block posts response:ready.
  • The popup receives ack:ready and response:ready and does the same. Every received message spawns an ack: and a response: in the twin, so growth is exponential: ack:response:response:ack:...:ready.
  • The storm also starves the real tester, so the run additionally fails with The iframe ... did not acknowledge the "cleanup" message within 60000ms.

The orchestrator already ignores response:/ack: events in its default: case ("ignore acknowledgements and responses to events we sent", added in #10656); the tester side had no equivalent protection against a duplicate participant.

This also explains the coverage report in #9379 (grant-progress): anything that opens the tester URL in a second browsing context triggers the same loop.

Fix

The tester only participates in the iframe channel when it is actually embedded in the orchestrator (window.self !== window.top): a top-level copy of the page neither handles channel messages nor announces ready. This removes the duplicate participant instead of filtering event names, addressing the review feedback that the previous startsWith('response:') guard only hid the symptom.

Numbers (fixture: one test clicking <a href="" target="_blank"> + one trivial follow-up file, chromium)

before after
result 2 tests pass, exit code 1 2 tests pass, exit code 0
unhandled errors 1,238,507 Unknown event: ... 0
duration 96.6s (cleanup ack timeout) 1.8s

Regression test

test/browser/specs/duplicate-tester.test.ts (same style as specs/readiness.test.ts): runs the popup-click scenario inline and asserts no Unknown event in stderr, exit code 0, and both tests passing.

Verified failing without the fix and passing with it on chromium, firefox and webkit:

  • unfixed: chromium exit 1 (579k Unknown event errors in ~42s), webkit exit 1 (446k errors), firefox exit 1 (inner run flooded until the spec's 60s timeout)
  • fixed: all three pass in 2–4s; specs/readiness.test.ts (3 tests over the same handshake machinery) still passes on all three

AI disclosure

Root-cause tracing, the fix, and the regression test were developed with assistance from Claude Code; I reviewed all changes and the verification runs above were executed locally.

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed.
  • Ideally, include a test that fails without this PR but passes with it.
  • Please, don't make changes to pnpm-lock.yaml unless you introduce a new test example.

Tests

  • Run the tests with pnpm test:ci. (duplicate-tester + readiness specs verified on chromium/firefox/webkit; a full local test/browser run hits pre-existing failures that reproduce identically on unmodified main in my environment, so relying on CI for the full matrix)

Documentation

  • If you introduce new functionality, document it. You can run documentation with pnpm run docs command.

Changesets

  • Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable way. Please, prefix changeset messages with feat:, fix:, perf:, docs:, or chore:.

Comment thread packages/browser/src/client/tester/tester.ts Outdated
@netlify

netlify Bot commented Jan 4, 2026

Copy link
Copy Markdown

Deploy Preview for vitest-dev ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit da0e8c0
🔍 Latest deploy log https://app.netlify.com/projects/vitest-dev/deploys/695afe174fad260008901f89
😎 Deploy Preview https://deploy-preview-9381--vitest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

… window (fix vitest-dev#9379)

A test can open the tester page as a top-level window, e.g. by clicking
an anchor with target="_blank" whose href resolves to the tester URL
(href="" resolves to the current page, ?sessionId=...&iframeId=...
included). The duplicate tester joins the BroadcastChannel with the same
iframeId and echoes ack:/response: events back to the real tester, each
received message spawning an ack and a response in the twin, generating
events recursively until the run fails with hundreds of thousands of
"Unknown event" errors and the orchestrator times out waiting for the
real tester's acknowledgements.

The tester now only participates in the iframe channel when it is
actually embedded in the orchestrator.
@simshanith
simshanith force-pushed the simshanith/response-event-loop-guard branch from da0e8c0 to 361c463 Compare July 7, 2026 03:33
@simshanith simshanith changed the title fix: guard against infinite response event generation (fix #9379) fix(browser): ignore channel events in a tester opened as a top-level window (fix #9379) Jul 7, 2026
@simshanith
simshanith requested a review from sheremet-va July 7, 2026 03:54
simshanith added a commit to simshanith/lit-ui-router that referenced this pull request Jul 7, 2026
…wser patch (#186)

Replaces the symptom filter (dropping ready/response:* events by name)
with the root-cause guard from vitest-dev/vitest#9381: a tester opened
as a top-level window (e.g. a test clicking <a target="_blank"> at the
tester URL) joins the BroadcastChannel with the same iframeId and echoes
response: events back recursively. The tester now only participates in
the iframe channel when actually embedded in the orchestrator.

Refs #69
@simshanith

Copy link
Copy Markdown
Author

@sheremet-va this is ready for another look — rewrote it from scratch, and your review was right on both counts:

  • BroadcastChannel indeed never echoes to the sender. The actual mechanism: the dev server serves a functional tester page to any window with a live sessionId, and a test that lets the browser's default action run on <a href="" target="_blank"> (href="" resolves to the tester URL, query string included) opens a popup that boots a second, top-level tester with the same iframeId — the two then ping-pong ack:/response: events exponentially. Full trace and a self-contained repro are now in Browser mode: Infinite 'Unknown event: response:response:...' loop when running multiple click tests #9379 (issue body updated today).
  • The startsWith('response:') guard is gone. The fix now removes the duplicate participant instead of filtering event names: the tester only joins the iframe channel when it is actually embedded in the orchestrator (window.self !== window.top) — the tester-side counterpart of the orchestrator guard from fix: add handshake timeout to iframe communication #10656.

New regression test test/browser/specs/duplicate-tester.test.ts (same style as specs/readiness.test.ts): on main it fails with ~580k Unknown event errors / exit 1; with the fix it passes in ~2s. Verified on chromium, firefox and webkit; specs/readiness.test.ts still passes on all three. The one failing browser shard here looks unrelated (mocking.test.ts watch-rerun + a firefox wheel.test.ts flake) — duplicate-tester itself passed on CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Browser mode: Infinite 'Unknown event: response:response:...' loop when running multiple click tests

2 participants